前一篇整理了 JIT Provisioning 與 SCIM,從首次登入時的帳號開通,一路談到後續的帳號同步、異動與停用。接下來要換個角度,從安全攻防來看 OAuth 2.0 與 OpenID Connect(OIDC)流程中可能出現的風險。
本篇會先從中間人攻擊(MITM)的概念開始,再逐步拉回 OAuth 2.0/OIDC 的 Authorization Code Flow,說明 state、nonce 與 PKCE 各自防範的風險,以及 RFC 9700 對這類攻擊提出的安全建議。
今天內容涵蓋:
中間人攻擊(Man-in-the-Middle Attack,MITM)指的是攻擊者站在使用者與網站/服務中間,讓雙方都以為連線正常。實際上,資料會先經過攻擊者,再被轉送到另一端。
攻擊者不一定只是偷看資料,也可能攔截、修改或重送資料。常見情境包括:偽造 Wi-Fi 熱點、ARP Spoofing、DNS Spoofing、HTTPS Stripping、Session Hijacking,以及透過偽造憑證的 SSL/TLS 攔截代理介入連線。

這些手法發生的位置不同,但共同點都是:原本應該直接往來的通訊,被攻擊者插入中間。也因為雙方表面上都還能正常收發資料,使用者不一定會立刻察覺異常。
所以後面談 OAuth 2.0/OIDC 時,不能只看「有沒有 HTTPS」。HTTPS 很重要,但授權請求、回調、授權碼交換與 Token 驗證這些流程,也都需要防止資料被攔截、竄改或重放。
回到 Authorization Code Flow 來看,OAuth 2.0/OIDC 的登入流程不是只有「使用者登入」這一步,而是由授權請求、授權回應、Code 交換、Token 發行,以及後續使用 Access Token 存取資源組成。
這張圖先把流程拆開來看:左邊是正常流程,右邊標出每個階段可能出現的風險。

可以先抓住四個重點:
| 圖中的階段 | 主要風險 | 後面對應的防護 |
|---|---|---|
| ① 授權請求 | 登入 CSRF/Code Injection:攻擊者把自己的登入結果或授權碼塞進受害者的流程 | state |
| ② 授權回應(重導) | 授權碼攔截:Authorization Code 在回到 Client 的途中被拿走 | PKCE |
| ③ Code 交換 | 攔截後直接換 Token:如果只有授權碼就能換 Token,攻擊者就可能搶先兌換 | PKCE |
| ④ Token 發行(OIDC) | ID Token Replay:舊的 ID Token 被重新拿來冒用身分 | nonce |
state、PKCE、nonce 之所以容易看起來很像,是因為它們都會在流程前段產生一個不容易被猜到的值,並在後面拿來檢查。不過它們出現的位置和驗證時機不同:
| 機制 | 在哪裡產生 | 放在哪個流程 | 什麼時候驗證 | 驗證目的 |
|---|---|---|---|---|
| state | Client 發起授權請求前 | 授權請求,之後由 Authorization Server 原樣帶回授權回應 | Client 收到 redirect_uri 回調時 | 確認授權回應是否屬於同一次流程 |
| PKCE | Client 發起授權請求前產生 code_verifier,並算出 code_challenge | 授權請求只放 code_challenge;Code 交換時才送出 code_verifier | Authorization Server 的 Token 端點收到 Code 交換請求時 | 確認拿 authorization code 換 Token 的是原本的 Client |
| nonce | Client 發起 OIDC 授權請求前 | 授權請求,之後由 Authorization Server 寫入 ID Token | Client 收到並驗證 ID Token 時 | 確認 ID Token 是否為本次登入流程簽發 |
因此,這一篇後面會照著圖上的攻擊點往下看:先看 state 如何把授權請求與授權回應配對,再看 PKCE 如何防止被攔截的 authorization code 被拿去換 Token,最後看 nonce 如何防止舊的 ID Token 被重新使用。至於圖中後續使用 Access Token 存取 Resource Server 的部分,會留到下一篇討論被竊取的 Access Token 要怎麼限制使用。
這些情境也呼應 RFC 9700 的安全建議:防護不能只放在單一環節,而要涵蓋授權請求、授權回應、Code 交換與 Token 驗證等不同階段。
state 對應的是圖中的第 ①、② 階段:Client 發出授權請求,以及 Authorization Server 透過 redirect_uri 把使用者導回 Client 的授權回應。
它要解決的問題是:Client 收到回應時,如何確認「這個回應真的接得上剛剛那次授權請求」,而不是攻擊者另外塞進來的回調。
正常流程如下:
攻擊情境:攻擊者先自己走一遍授權流程,拿到一組屬於攻擊者帳號的有效 authorization code,再把這組 code 包進回調連結中,例如 https://app.com/callback?code=ATTACKER_CODE,誘騙受害者點擊。若 Client 沒有檢查 state,就可能把這個外部塞進來的回應當成合法流程處理,導致受害者的登入狀態被綁定到攻擊者帳號,形成登入 CSRF(Cross-Site Request Forgery,跨站請求偽造)或 Code Injection(授權碼注入)。
所以,state 保護的重點是「回應是否屬於同一次授權流程」。它不負責保護 authorization code 本身不被偷走;如果 code 已經在回到 Client 的途中被攔截,那是下一節 PKCE 要處理的問題。
📘RFC 9700 明確要求:客戶端必須防範 CSRF。若客戶端確認授權伺服器支援 PKCE,可以直接依賴 PKCE 提供的 CSRF 保護;在 OIDC 流程中 nonce 也能提供 CSRF 保護;否則就必須使用綁定到 user agent 的一次性 state。
PKCE 對應的是圖中的第 ②、③ 階段:Authorization Code 回到 Client 的路上,以及 Client 拿 Authorization Code 去交換 Token 的過程。
它和 state 看起來相似,都是在把前後流程接起來;但兩者檢查的重點不同:
攻擊情境:如果攻擊者在授權回應階段攔截到 Authorization Code,例如行動裝置上有惡意 App 註冊了相同的 URL scheme(例如 my-app://callback),就可能搶先拿這組 code 去 Token 端點交換 Token。若 Authorization Server 只檢查 code 本身,攻擊者就有機會成功。
PKCE 的做法是讓 Client 在一開始就準備一組「只有自己知道的驗證值」:
code_verifier 之所以能作為防護,是因為它不會出現在前面的瀏覽器重導流程中。攻擊者即使在 redirect_uri 回來時攔截到 authorization code,通常也只能拿到 code,看不到一開始保存在 Client 端的 code_verifier。沒有 code_verifier,就無法完成 Token 交換。
因此,PKCE 的重點不是讓 authorization code 不會被偷,而是讓「偷到 code 的人仍然不能用它換 Token」。
📘RFC 9700 要求公開客戶端必須使用 PKCE,機密客戶端也建議使用。challenge 方法必須使用 S256,避免使用 plain;授權伺服器也必須避免 PKCE 降級攻擊,不能讓攻擊者把原本需要 code_verifier 的流程降級成不檢查。
nonce 對應的是圖中的第 ④ 階段:Authorization Server 發出 ID Token,Client 接收並驗證登入結果的時候。
nonce 的重點在於 ID Token 本身。即使 ID Token 的簽章有效,也只能代表它曾經由 Authorization Server 簽發;Client 仍需要確認它是不是本次登入流程產生的身分結果。也就是說,這裡要防的是「舊的 ID Token 被重新拿來使用」,而不是前面 state 處理的回調配對問題。
ID Token 是 OIDC 用來表示使用者身分的 Token。因為它有 Authorization Server 的簽章,所以 Client 會依據它判斷使用者是誰。問題是:簽章有效只代表「這個 ID Token 曾經由 Authorization Server 簽發」,不代表「它一定是這一次登入流程產生的」。
攻擊流程可能會長這樣:
nonce 的做法,是讓 Client 在授權請求階段先產生一組不可猜的 nonce,並把它與本次登入流程一起保存。接著,Client 把 nonce 放進授權請求送給 Authorization Server。
Authorization Server 簽發 ID Token 時,會把同一組 nonce 寫進 ID Token。Client 收到 ID Token 後,除了驗證簽章、issuer、audience、有效時間等資訊,也要檢查 ID Token 裡的 nonce 是否與自己本次流程保存的 nonce 一致。
如果 nonce 不一致,表示這個 ID Token 不是針對本次登入流程簽發;即使它的簽章有效,也應該拒絕。
因此,nonce 的重點不是確認「回調是不是同一次流程」,而是確認「ID Token 是不是本次流程產生的身分結果」。這也是它和 state 最大的差異。
📘RFC 9700 提到,在符合特定條件的機密客戶端中,nonce 可以提供與 PKCE 類似的保護效果;但對公開客戶端而言,PKCE 仍然是必要防護。實作時可以把 nonce 視為 OIDC 用來防止 ID Token Replay 的重要檢查。
RFC 9700(BCP 240,Best Current Practice for OAuth 2.0 Security)是 OAuth 2.0 的安全最佳實踐文件,於 2025 年 1 月發布。它整理了 RFC 6749、RFC 6750、RFC 6819 之後累積的安全經驗,並針對實務上常見的攻擊手法,明確建議應採取的防護措施。
對本文討論的 MITM 與授權流程攻擊而言,RFC 9700 的重點不只是要求使用單一防護機制,而是要求 Client、Authorization Server 與 Resource Server 在不同階段各自落實必要檢查。以下整理幾項與本文主題相關的重點:
也就是說,RFC 9700 的重點不是新增單一機制,而是把 OAuth 2.0 流程中各階段該做的安全檢查收斂成一組實務基準。
對 OAuth 2.0/OIDC 而言,MITM 風險不只存在於網路傳輸層,也可能出現在授權請求、授權回應、授權碼交換與 Token 驗證等不同階段。
本文提到的 state、PKCE 與 nonce,分別對應登入 CSRF/Code Injection、授權碼攔截,以及 ID Token Replay。它們不是彼此替代的選項,而是在不同位置補上不同的安全檢查。
RFC 9700 則把這些防護整理成 OAuth 2.0 的安全最佳實踐,提醒實作者除了正確使用 state、PKCE 與 nonce,也要落實 redirect_uri 精確比對、避免不安全的授權方式,並持續檢視 Token 在後續使用階段可能面臨的風險。
下一篇會接著看 Access Token 發出之後的風險:如果 Token 真的被偷走,如何透過權限範圍、短效期限,以及 mTLS/DPoP 這類 Sender-Constrained Token 機制,限制它被攻擊者直接冒用。